feat(l1): reject blob-tx replacements that shrink the blob count#6624
feat(l1): reject blob-tx replacements that shrink the blob count#6624ilitteri wants to merge 2 commits into
Conversation
Adds a blob-count invariant to the mempool RBF path: an EIP-4844
replacement that carries fewer blobs than the in-pool transaction is
rejected even when fee fields are sufficiently bumped. A smaller
sidecar costs the network less to gossip, so without this check an
attacker could cycle "replace N-blob tx with 1-blob tx at the required
bump" cheaply.
The new check fires before the existing fee-bump logic in
`find_tx_to_replace`, and is paired with a new
`MempoolError::ReplacementShrinksBlobs { old_count, new_count }`
variant. The fee-bump arithmetic is untouched.
Closes part of #6623.
🤖 Kimi Code ReviewThe PR looks solid. This correctly implements protection against blob-count griefing attacks in the mempool (where a user replaces a high-blob transaction with a low-blob one to cheaply cycle gossip). Feedback: 1. Test completeness in // After the match block in rejects_replacement_with_fewer_blobs_even_with_doubled_fees
assert!(mempool.contains(&old.hash())); // Ensure old tx wasn't evicted2. Code clarity in blob comparison (line ~475) let shrinking_blobs = new_count < old_count;
if shrinking_blobs {
return Err(...);
}This reads slightly better than the inline comparison, though this is subjective. 3. TODO comment accuracy (line ~468) Security verification:
Nitpick: Conclusion: LGTM. The logic is correct, well-documented, and the tests cover the three critical cases (fewer, equal, greater blob counts). Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt |
🤖 Claude Code ReviewPR Review: feat(l1): reject blob-tx replacements that shrink the blob countOverviewThis PR closes a residual blob-spam vector left open after #6601. An attacker could satisfy an RBF fee bump while replacing a many-blob EIP-4844 tx with a fewer-blob one, cycling cheaply to amplify blob churn relative to fees paid. The fix adds a blob-count monotonicity invariant to CorrectnessThe core logic is correct. The guard at Cross-type RBF gap (design limitation — worth documenting explicitly). The The TODO comment acknowledges this will be covered by the type-discriminant check in #6601 — that's a reasonable forward reference. But the current test suite has no case documenting this expected behavioral gap, which makes the limitation invisible to future readers. Consider adding: #[test]
fn cross_type_replacement_eip4844_to_eip1559_bypasses_blob_check() {
// Documents the current intentional gap:
// replacing EIP-4844 with EIP-1559 bypasses the blob-count invariant
// (will be closed by PR #6601's type-discriminant guard).
...
}Existing pre-existing concern (out of scope, but worth noting)The fee check at new_blob_fee > old_blob_feeThis is a pre-existing issue unrelated to this PR, but the description says #6601 "introduces percentage fee bumps" — it would be worth verifying the integration once #6601 lands. TestsThe three tests (
One minor improvement — the Error Variant
SummaryThe implementation is focused, minimal, and correctly closes the same-type blob-shrink vector. The main gap is cross-type RBF (EIP4844 → EIP1559), which is a known limitation acknowledged by the TODO, but worth making explicit with a test. No other correctness, security, or safety issues found. Automated review by Claude (Anthropic) · sonnet · custom prompt |
Greptile SummaryThis PR closes a residual blob-spam vector in the mempool's Replace-By-Fee logic: a caller could previously satisfy the fee-bump threshold while replacing a 6-blob EIP-4844 transaction with a 1-blob one, amplifying blob churn at low cost. The fix adds a single guard in
Confidence Score: 4/5Safe to merge; the change is purely additive and tightens RBF admission for EIP-4844 transactions. The blob-count guard fires correctly before the fee check, and the three tests exercise the reject/same/grow paths against real pool state. The only remaining gap — replacing an EIP-4844 pooled tx with a non-EIP-4844 tx bypasses this check entirely — is pre-existing, acknowledged with a TODO, and tracked in #6601/#6623. Nothing in this PR regresses existing behaviour. No files require special attention, though
|
| Filename | Overview |
|---|---|
| crates/blockchain/mempool.rs | Adds blob-count guard to find_tx_to_replace and three unit tests; logic is correct for EIP4844→EIP4844 replacements; EIP4844→non-EIP4844 cross-type replacement gap is pre-existing and tracked in #6623/#6601. |
| crates/blockchain/error.rs | Adds ReplacementShrinksBlobs { old_count, new_count } error variant with a clear message; no issues found. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[find_tx_to_replace called] --> B{contains_sender_nonce?}
B -->|None| C[return Ok None]
B -->|Some tx_in_pool| D{Both txs are EIP4844Transaction?}
D -->|No| F[fee bump check]
D -->|Yes| E{new blob count less than old blob count?}
E -->|Yes| G[return Err ReplacementShrinksBlobs]
E -->|No - same or more| F
F -->|fails| H[return Err UnderpricedReplacement]
F -->|passes| I[return Ok Some old_hash]
Reviews (1): Last reviewed commit: "feat(l1): reject blob-tx replacements th..." | Re-trigger Greptile
Lines of code reportTotal lines added: Detailed view |
There was a problem hiding this comment.
Pull request overview
This PR hardens L1 mempool RBF behavior for EIP-4844 transactions by preventing replacements that reduce the blob count, closing a residual “blob churn” spam vector not fully addressed by fee-bump rules alone.
Changes:
- Add an invariant in
Mempool::find_tx_to_replacethat rejects EIP-4844 → EIP-4844 replacements whenblob_versioned_hashes.len()decreases. - Introduce
MempoolError::ReplacementShrinksBlobs { old_count, new_count }with a descriptive error message. - Add unit tests covering shrinking, same-count, and growing-count blob replacements.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| crates/blockchain/mempool.rs | Adds blob-count shrink rejection to RBF logic and introduces unit tests for the new behavior. |
| crates/blockchain/error.rs | Adds a new MempoolError variant for blob-shrinking replacements. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if let (Transaction::EIP4844Transaction(old_tx), Transaction::EIP4844Transaction(new_tx)) = | ||
| (tx_in_pool.transaction(), tx) | ||
| { | ||
| let old_count = old_tx.blob_versioned_hashes.len(); | ||
| let new_count = new_tx.blob_versioned_hashes.len(); | ||
| if new_count < old_count { | ||
| return Err(MempoolError::ReplacementShrinksBlobs { | ||
| old_count, | ||
| new_count, | ||
| }); | ||
| } | ||
| } |
🤖 Codex Code ReviewFindings
No other correctness or security issues stood out in the diff. I couldn’t run Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
Phase 2 cross-check found that the original PR guard only checks the blob-count when BOTH old and new are EIP-4844. A non-blob tx replacing an in-pool blob tx is treated as a degenerate shrink to 0 blobs and rejected with the same error. Removes this PR's dependency on PR #6601's type-discriminant check landing first to close the cross-type spam vector. The TODO is updated to note that the cross-type arm becomes unreachable once #6601 merges. New test: rejects_non_blob_replacement_of_blob_tx.
Motivation
PR #6601 introduces percentage fee bumps (10%/100%) for mempool RBF, which closes the bulk of the replacement-spam vector. However, a residual blob-spam vector remains: an attacker can satisfy the fee bump while replacing a multi-blob in-pool transaction with a single-blob one. The smaller sidecar costs the network less to gossip, so the attacker can cycle "replace 6-blob tx with 1-blob tx at the required bump" repeatedly and amplify blob churn relative to fees paid. Nethermind already enforces this invariant; ethrex did not.
Description
Adds a blob-count invariant to
Mempool::find_tx_to_replace: when both the in-pool transaction and the candidate replacement areTransaction::EIP4844Transaction, the replacement is rejected if it carries fewerblob_versioned_hashesthan the in-pool tx. The check fires before the per-fee-field bump check, so a "shrinking" replacement is rejected even when fees are bumped above the threshold.A new error variant
MempoolError::ReplacementShrinksBlobs { old_count, new_count }is added with a descriptive message.The fee-bump arithmetic itself is untouched — this PR is purely additive.
Behavioral change: RBF is strictly stricter for EIP-4844 transactions that attempt to shrink the blob count. Same-count and growing-count replacements continue to work exactly as before (assuming the existing fee gates pass).
A TODO in the code notes that once PR #6601 lands, the type-discriminant guard there subsumes the explicit
match, and this block can be simplified.Closes part of #6623.
Checklist
STORE_SCHEMA_VERSION(crates/storage/lib.rs) if the PR includes breaking changes to theStorerequiring a re-sync.